/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package maxbe.rel2xml.grammar;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import maxbe.rel2xml.generator.Column;
import maxbe.rel2xml.generator.Table;
import maxbe.rel2xml.generator.docs.constraints.DataConstraint;
import maxbe.rel2xml.generator.docs.constraints.DerivedValues;
import maxbe.rel2xml.generator.docs.constraints.References;
import maxbe.rel2xml.generator.docs.constraints.UniqueNumbers;
import maxbe.rel2xml.generator.docs.constraints.ValueConstructor;

import org.apache.log4j.Logger;

/**
 * Backend class holding schema information collected by CreateTableParser
 * 
 * @author Max Bechtold
 * 
 */
public class SchemaBuilder {

	private static Logger log = Logger.getLogger(SchemaBuilder.class);

	final List<Table> tables = new ArrayList<Table>();
	final Map<Column, String> references = new HashMap<Column, String>();
	final List<Column> refFrom = new ArrayList<Column>();
	final List<String> refTo = new ArrayList<String>();

	public List<Table> getTables() {
		return tables;
	}
	
	/**
	 * Checks if there is already a table under a given name.
	 */
	void checkTable(String name) {
		Table table = null;
		for (Table table2 : tables) {
			if (table2.getName().equals(name)) {
				table = table2;
			}
		}

		if (table != null) {
			String msg = String.format("Table '%s' exists already.", name);
			log.error(msg);
			throw (new SchemaException(msg));
		}
	}

	/**
	 * Adds a column to a table.
	 */
	void addColumn(Table table, Column column) {
		Column column2 = table.getColumn(column.getName());
		if (column2 != null) {
			String msg = String.format(
					"Table '%s' already has a column named '%s'.",
					table.getName(), column.getName());
			log.error(msg);
			throw (new SchemaException(msg));
		}
		table.addColumn(column);
	}

	/**
	 * Retrieves the column of a given name from a table.
	 */
	Column getColumn(Table table, String name) {
		Column column = table.getColumn(name);
		if (column == null) {
			String msg = String.format(
					"Table '%s' does not have a column named '%s'.",
					table.getName(), name);
			log.error(msg);
			throw new SchemaException(msg);
		}
		return column;
	}
	
	/**
	 * Retrieves the column given as parameters. This might also be the 'all'
	 * column wildcard.
	 */
	Column getReferredColumn(Table table, String name) {
		if ("*".equals(name)) {
			// Create pseudo column
			Column c = new Column("*", null, null, null);
			c.setTable(table);
			return c;
		}
		return getColumn(table, name);
	}

	/**
	 * Resolves dangling references and interconnects columns.
	 */
	void resolveReferences() {
		for (Column col : references.keySet()) {
			String reference = references.get(col);
			Column refColumn = resolveReference(col, reference);
			col.setRefColumn(refColumn);
		}
	}

	/**
	 * Resolve the reference of a column.
	 */
	Column resolveReference(Column col, String reference) {
		String[] split = reference.split("\\.");
		Column refColumn = null;
		for (Table table : tables) {
			if (table.getName().equals(split[0])) {
				refColumn = table.getColumn(split[1]);
				break;
			}
		}

		if (refColumn == null) {
			String msg = String.format(
					"Unable to resolve reference %s(%s) in %s.%s.", split[0],
					split[1], col.getTable().getName(), col.getName());
			log.error(msg);
			throw (new SchemaException(msg));
		} else if (!refColumn.isUnique() && refColumn.getCompositeKeyId() == 0) {
			String msg = String
					.format("Referenced column %s(%s) in %s.%s is not declared as unique and thus cannot be referenced.",
							split[0], split[1], col.getTable().getName(),
							col.getName());
			log.error(msg);
			throw (new SchemaException(msg));
		} else if (!col.getDataType().equals(refColumn.getDataType())) {
			String msg = String
					.format("Referenced column %s(%s) in %s.%s does not match the required DataType '%s'.",
							split[0], split[1], col.getTable().getName(),
							col.getName(), col.getDataType());
			log.error(msg);
			throw (new SchemaException(msg));
		} else if (col.getLength() != refColumn.getLength()
				|| col.getSubLength() != refColumn.getSubLength()) {
			String msg = String
					.format("Referenced column %s(%s) in %s.%s does not have the same length or subLength.",
							split[0], split[1], col.getTable().getName(),
							col.getName(), col.getDataType());
			log.error(msg);
			throw (new SchemaException(msg));
		}
		return refColumn;
	}

	/**
	 * Checks if a reference consists of the same number of columns in both the
	 * from and to table.
	 */
	void checkEqualSizedReference(Table table) {
		if (refTo.size() != refFrom.size()) {
			String msg = String
					.format("A foreign key specification in table '%s' has an unequal number of from and to columns.",
							table.getName());
			log.error(msg);
			throw (new SchemaException(msg));
		}
	}

	void checkCompositeKeys() {
		Iterator<Column> refFroms = refFrom.iterator();
		Iterator<String> refTos = refTo.iterator();
		if (!refFroms.hasNext())
			return;

		Column column = null;
		Column refColumn = null;
		Column oldColumn = null;

		int compositeForeignKeyId = 0;
		int compositeKeyId = 0;
		List<Column> refColumns = new ArrayList<Column>();
		boolean firstLoop = true;
		Table refTable = null;

		while (refFroms.hasNext()) {
			oldColumn = column;
			column = refFroms.next();
			refColumn = resolveReference(column, refTos.next());
			column.setRefColumn(refColumn);

			if (firstLoop) {
				firstLoop = false;
				compositeForeignKeyId = column.getCompositeForeignKeyId();
				compositeKeyId = refColumn.getCompositeKeyId();
				refTable = refColumn.getTable();
			} else if (column.getCompositeForeignKeyId() != compositeForeignKeyId) {
				// Coming to a new composite foreign key
				validateCompositeFK(oldColumn, refTable, compositeKeyId,
						refColumns.size());
				refColumns.clear();
				refTable = refColumn.getTable();

				compositeForeignKeyId = column.getCompositeForeignKeyId();
				compositeKeyId = refColumn.getCompositeKeyId();
			}

			if (refColumn.getCompositeKeyId() != compositeKeyId) {
				String msg = String
						.format("Column %s.%s is part of a composite foreign "
								+ "key but does not reference the same composite "
								+ "unique or primary key as the other columns in that key.",
								column.getTable().getName(), column.getName());
				log.error(msg);
				throw (new SchemaException(msg));
			}

			if (refColumns.contains(refColumn)) {
				String msg = String
						.format("Column %s.%s is part of a composite foreign key and "
								+ "references the same column as another column of that key.",
								column.getTable().getName(), column.getName());
				log.error(msg);
				throw (new SchemaException(msg));
			}

			refColumns.add(refColumn);
		}

		validateCompositeFK(column, refTable, compositeKeyId, refColumns.size());

	}

	/**
	 * Checks if a composite foreign key is well-formed.
	 */
	private static void validateCompositeFK(Column lastColumn, Table refTable,
			int compositeKeyId, int referenced) {
		// Check if composite key actually consists of multiple columns
		if (referenced == 1) {
			lastColumn.setCompositeForeignKeyId(0);
		}

		// Check if referenced table contains more columns of referenced
		// composite key
		int count = 0;

		for (Column col : refTable.getColumns())
			if (col.getCompositeKeyId() == compositeKeyId)
				count++;

		if (count != referenced) {
			String msg = String
					.format("A composite foreign key in table '%s' does not reference a complete composite key.",
							lastColumn.getTable().getName());
			log.error(msg);
			throw new SchemaException(msg);
		}
	}
	
	/**
	 * Retrieves the table of the given name.
	 */
	Table getTable(String name) {
		Table table = null;
		for (Table table2 : tables) {
			if (table2.getName().equals(name)) {
				table = table2;
			}
		}

		if (table == null) {
			String msg = String.format("Table '%s' has not been defined.", 
					name);
			log.error(msg);
			throw new SchemaException(msg);
		}
		return table;
	}
	
	/**
	 * Checks the difference contexts per column.
	 */
	void checkContexts() {
		for (Table table : tables) {
			for (Column column : table.getColumns()) {
				DataConstraint dc = column.getDataConstraint();
				checkPartialContext(table, column, dc);

				ValueConstructor vc = dc.getValueConstructor();
				if (vc instanceof References
						&& ((References) vc).getRefContext() != null) {
					checkReference(table, column, vc);
				}

				if (vc instanceof UniqueNumbers
						&& !((UniqueNumbers) vc).isGlobal()
						|| column.isDerived()) {
					checkUniqueNumbers(table, column, vc);
				}
			}
		}
	}

	protected void checkPartialContext(Table table, Column column,
			DataConstraint dc) {
		Table partialContext = dc.getPartialContext();
		if (partialContext != null) {
			if (partialContext == table) {
				String msg = String.format("Column '%s.%s' cannot have its " +
						"own table defined as its partial context as this " +
						"is meaningless.", table.getName(), column.getName());
				log.error(msg);
				throw (new FillTableException(msg));
			}

			if (!table.dependsOn(partialContext, true, true)) {
				String msg = String
						.format("Column '%s.%s' cannot have table '%s' " +
								"defined as its partial context as the table " +
								"is not in the dependency hierarchy.",
								table.getName(), column.getName(),
								partialContext.getName());
				log.error(msg);
				throw (new FillTableException(msg));
			}

			column.setMarkInterval(table.getBase()
					/ table.howManyIterationsThrough(partialContext));
		}
	}

	protected void checkReference(Table table, Column column,
			ValueConstructor vc) {
		Table context = ((References) vc).getRefContext();
		Table target = column.getRefColumn().getTable();
	
		if (!table.dependsOn(context, false, true)) {
			String msg = String.format("Column '%s.%s' cannot reference per" +
					" tuple of table '%s' as there is no chain of primary " +
					"dependencies defined between the tables.",table.getName(),
					column.getName(), context.getName());
			log.error(msg);
			throw (new FillTableException(msg));
		}
	
		if (!target.dependsOn(context, false, true)) {
			String msg = String
					.format("Column '%s.%s' cannot reference per tuple of " +
							"table '%s' as there is no chain of primary " +
							"dependencies defined between the referenced " +
							"table and the context table.", table.getName(), 
							column.getName(), context.getName());  
			log.error(msg);
			throw (new FillTableException(msg));
		}
	
		column.setMarkInterval(table.getBase()
				/ table.howManyIterationsThrough(context));
	}

	protected void checkUniqueNumbers(Table table, Column column,
			ValueConstructor vc) {
		Table target = null;
		boolean cascade = false;
		int factor = table.getCardinality();
	
		if (vc instanceof UniqueNumbers) {
			target = ((UniqueNumbers) vc).getUniqueContext();
			cascade = false;
	
			if (target == column.getTable()) {
				String msg = String
						.format("Column '%s.%s' cannot be unique per tuple " +
								"of its own table as this is meaningless.",
								table.getName(), column.getName());
				log.error(msg);
				throw (new FillTableException(msg));
			}
		} else if (vc instanceof DerivedValues) {
			target = ((DerivedValues) vc).getSourceColumn().getTable();
			cascade = true;
		}
	
		if (target == null) {
			// Meaning we want to have uniqueness on the lowest level
			column.setMarkInterval(1);
		} else {
			if (target != column.getTable()) {
				if (!table.dependsOn(target, cascade, true)) {
					String msg = null;
					if (vc instanceof UniqueNumbers) {
						msg = String.format("Column '%s.%s' cannot be unique " +
								"per tupel of table '%s' as there is no chain " +
								"of primary dependencies defined between the " +
								"tables.", table.getName(), column.getName(),
								target.getName());
					} else if (vc instanceof DerivedValues) {
						msg = String.format("Column '%s.%s' cannot be derived " +
								"from table '%s' as there is no dependency " +
								"defined between the tables.", table.getName(),
								column.getName(), target.getName());
					}
	
					log.error(msg);
					throw (new FillTableException(msg));
				}
	
				factor = table.howManyIterationsThrough(target);
			}
	
			column.setMarkInterval(table.getBase() / factor);
		}
	}
	

	/**
	 * Checks if generation rules for a table are complete.
	 */
	void checkTable(Table t) {
		for (Column column : t.getColumns()) {
			if (column.getDataConstraint() == null) {
				String msg = String.format("Column '%s.%s' misses a value " +
						"generation rule.", t.getName(), column.getName());
				log.error(msg);
				throw (new FillTableException(msg));
			}
		}
	}
}
